Valores missing, outlier y correlaciones¶

En este notebook se realiza el estudio y preprocesamiento de las variables numéricas y categoricas. Se realizarán los siguientes pasos:

  1. Cambio de tipos de variables
  2. Separación en train y test
  3. Análisis de cada variable con gráficos descriptivos
  4. Para variables numericas: correlaciones de pearnson, estudio de outliers y estudio de valores missing
  5. Para variables categoricas: relleno de valores missing, estudio de correlaciones con vCramer

Importo librerías¶

In [2]:
import pandas as pd
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
import plotly.express as px
from sklearn.impute import KNNImputer
import scipy.stats as ss
import warnings
from sklearn.model_selection import train_test_split
warnings.filterwarnings('ignore')

pd.set_option('display.max_columns', 500)
pd.set_option('display.max_rows', 5000)

Funciones¶

In [3]:
def plot_feature(df, col_name, isContinuous, target):
    """
    Visualize a variable with and without faceting on the loan status.
    - df dataframe
    - col_name is the variable name in the dataframe
    - full_name is the full variable name
    - continuous is True if the variable is continuous, False otherwise
    """
    f, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(12,3), dpi=90)
    
    count_null = df[col_name].isnull().sum()
    if isContinuous:
        
        sns.histplot(df.loc[df[col_name].notnull(), col_name], kde=False, ax=ax1)
    else:
        sns.countplot(df, x=col_name, color='#5975A4', saturation=1, ax=ax1)
    ax1.set_xlabel(col_name)
    ax1.set_ylabel('Count')
    ax1.set_title(col_name+ ' Numero de nulos: '+str(count_null))
    plt.xticks(rotation = 90)


    if isContinuous:
        sns.boxplot(x=col_name, y=target, data=df, ax=ax2)
        ax2.set_ylabel('')
        ax2.set_title(col_name + ' by '+target)
    else:
        data = df.groupby(col_name)[target].value_counts(normalize=True).to_frame('proportion').reset_index() 
        data.columns = [i, target, 'proportion']
        #sns.barplot(x = col_name, y = 'proportion', hue= target, data = data, saturation=1, ax=ax2)
        sns.barplot(x = col_name, y = 'proportion', hue= target, data = data, saturation=1, ax=ax2)
        ax2.set_ylabel(target+' fraction')
        ax2.set_title(target)
        plt.xticks(rotation = 90)
    ax2.set_xlabel(col_name)
    
    plt.tight_layout()
    

def dame_variables_categoricas(dataset=None):
    '''
    ----------------------------------------------------------------------------------------------------------
    Función dame_variables_categoricas:
    ----------------------------------------------------------------------------------------------------------
        -Descripción: Función que recibe un dataset y devuelve una lista con los nombres de las 
        variables categóricas
        -Inputs: 
            -- dataset: Pandas dataframe que contiene los datos
        -Return:
            -- lista_variables_categoricas: lista con los nombres de las variables categóricas del
            dataset de entrada con menos de 100 valores diferentes
            -- 1: la ejecución es incorrecta
    '''
    if dataset is None:
        print(u'\nFaltan argumentos por pasar a la función')
        return 1
    lista_variables_categoricas = []
    other = []
    for i in dataset.columns:
        if (dataset[i].dtype!=float) & (dataset[i].dtype!=int):
            unicos = int(len(np.unique(dataset[i].dropna(axis=0, how='all'))))
            if unicos < 100:
                lista_variables_categoricas.append(i)
            else:
                other.append(i)

    return lista_variables_categoricas, other


def get_corr_matrix(dataset = None, metodo='pearson', size_figure=[10,8]):
    # Para obtener la correlación de Spearman, sólo cambiar el metodo por 'spearman'

    if dataset is None:
        print(u'\nHace falta pasar argumentos a la función')
        return 1
    sns.set(style="white")
    # Compute the correlation matrix
    corr = dataset.corr(method=metodo) 
    # Set self-correlation to zero to avoid distraction
    for i in range(corr.shape[0]):
        corr.iloc[i, i] = 0
    # Set up the matplotlib figure
    f, ax = plt.subplots(figsize=size_figure)
    # Draw the heatmap with the mask and correct aspect ratio
    sns.heatmap(corr, center=0,
                square=True, linewidths=.5,  cmap ='viridis' ) #cbar_kws={"shrink": .5}
    plt.show()
    
    return 0

def get_deviation_of_mean_perc(pd_loan, list_var_continuous, target, multiplier):
    """
    Devuelve el porcentaje de valores que exceden del intervalo de confianza
    :type series:
    :param multiplier:
    :return:
    """
    pd_final = pd.DataFrame()
    
    for i in list_var_continuous:
        
        series_mean = pd_loan[i].mean()
        series_std = pd_loan[i].std()
        std_amp = multiplier * series_std
        left = series_mean - std_amp
        right = series_mean + std_amp
        size_s = pd_loan[i].size
        
        perc_goods = pd_loan[i][(pd_loan[i] >= left) & (pd_loan[i] <= right)].size/size_s
        perc_excess = pd_loan[i][(pd_loan[i] < left) | (pd_loan[i] > right)].size/size_s
        
        if perc_excess>0:    
            pd_concat_percent = pd.DataFrame(pd_loan[target][(pd_loan[i] < left) | (pd_loan[i] > right)]\
                                            .value_counts(normalize=True).reset_index()).T
            pd_concat_percent.columns = [pd_concat_percent.iloc[0,0],
                                         pd_concat_percent.iloc[0,1]]
            #print('las columnas son', pd_concat_percent.columns, pd_concat_percent)
            pd_concat_percent = pd_concat_percent#.drop('index',axis=0)
            pd_concat_percent['variable'] = i
            pd_concat_percent['sum_outlier_values'] = pd_loan[i][(pd_loan[i] < left) | (pd_loan[i] > right)].size
            pd_concat_percent['porcentaje_sum_null_values'] = perc_excess
            pd_final = pd.concat([pd_final, pd_concat_percent], axis=0).reset_index(drop=True)
            
    if pd_final.empty:
        print('No existen variables con valores nulos')
        
    return pd_final


def get_percent_null_values_target(pd_loan, list_var_continuous, target):

    pd_final = pd.DataFrame()
    for i in list_var_continuous:
        if pd_loan[i].isnull().sum()>0:
            pd_concat_percent = pd.DataFrame(pd_loan[target][pd_loan[i].isnull()]\
                                            .value_counts(normalize=True).reset_index()).T
            pd_concat_percent.columns = [pd_concat_percent.iloc[0,0], 
                                         pd_concat_percent.iloc[0,1]]
            pd_concat_percent = pd_concat_percent.drop('index',axis=0)
            pd_concat_percent['variable'] = i
            pd_concat_percent['sum_null_values'] = pd_loan[i].isnull().sum()
            pd_concat_percent['porcentaje_sum_null_values'] = pd_loan[i].isnull().sum()/pd_loan.shape[0]
            pd_final = pd.concat([pd_final, pd_concat_percent], axis=0).reset_index(drop=True)
            
    if pd_final.empty:
        print('No existen variables con valores nulos')
        
    return pd_final



def cramers_v(confusion_matrix):
    """ 
    calculate Cramers V statistic for categorial-categorial association.
    uses correction from Bergsma and Wicher,
    Journal of the Korean Statistical Society 42 (2013): 323-328
    
    confusion_matrix: tabla creada con pd.crosstab()
    
    """
    chi2 = ss.chi2_contingency(confusion_matrix)[0]
    n = confusion_matrix.sum()
    phi2 = chi2 / n
    r, k = confusion_matrix.shape
    phi2corr = max(0, phi2 - ((k-1)*(r-1))/(n-1))
    rcorr = r - ((r-1)**2)/(n-1)
    kcorr = k - ((k-1)**2)/(n-1)
    return np.sqrt(phi2corr / min((kcorr-1), (rcorr-1)))

Lectura de datos del preprocesado inicial¶

Lectura de los datos y cambio de tipos de variables, en esta parte del código vamos a leer los datos. Mediante la función shape vamos a observar cuales son las dimensiones del dataframe que vamos a analizar. En concreto, el data frame base, tiene 1 millón de filas y se compone de 32 columnas.

Gracias a la función columns podemos saber el nombre de cada una de las 32 columnas que componen a nuestro dataset.

In [4]:
path_folder = "./"
df_base = pd.read_csv(path_folder +"Base.csv", low_memory=False)
df_base.shape
Out[4]:
(1000000, 32)
In [5]:
df_base.columns
Out[5]:
Index(['fraud_bool', 'income', 'name_email_similarity',
       'prev_address_months_count', 'current_address_months_count',
       'customer_age', 'days_since_request', 'intended_balcon_amount',
       'payment_type', 'zip_count_4w', 'velocity_6h', 'velocity_24h',
       'velocity_4w', 'bank_branch_count_8w',
       'date_of_birth_distinct_emails_4w', 'employment_status',
       'credit_risk_score', 'email_is_free', 'housing_status',
       'phone_home_valid', 'phone_mobile_valid', 'bank_months_count',
       'has_other_cards', 'proposed_credit_limit', 'foreign_request', 'source',
       'session_length_in_minutes', 'device_os', 'keep_alive_session',
       'device_distinct_emails_8w', 'device_fraud_count', 'month'],
      dtype='object')

Una de las funciones que se han creado para realizar este analisis es la funcion _dame_variablescategoricas. Esta función nos permite diferenciar dentro de un dataset si las variables son numéricas o categoricas. Esto es en base a si la variable tiene más de 100 valores distintos.

In [6]:
list_var_cat, other = dame_variables_categoricas(dataset=df_base)
df_base[list_var_cat] = df_base[list_var_cat].astype("category")
list_var_continuous = list(df_base.select_dtypes('float').columns)
df_base[list_var_continuous] = df_base[list_var_continuous].astype(float)
df_base.dtypes
Out[6]:
fraud_bool                          category
income                               float64
name_email_similarity                float64
prev_address_months_count              int64
current_address_months_count           int64
customer_age                        category
days_since_request                   float64
intended_balcon_amount               float64
payment_type                        category
zip_count_4w                           int64
velocity_6h                          float64
velocity_24h                         float64
velocity_4w                          float64
bank_branch_count_8w                   int64
date_of_birth_distinct_emails_4w    category
employment_status                   category
credit_risk_score                      int64
email_is_free                       category
housing_status                      category
phone_home_valid                    category
phone_mobile_valid                  category
bank_months_count                   category
has_other_cards                     category
proposed_credit_limit                float64
foreign_request                     category
source                              category
session_length_in_minutes            float64
device_os                           category
keep_alive_session                  category
device_distinct_emails_8w           category
device_fraud_count                  category
month                               category
dtype: object

Separación en train y test estratificado¶

Como se ha mencionado en el 01_notebook nuestra variable objetivo tiene un distribución muy descompensada que después de realizar el análisis EDA convendría llevar a cabo un oversampling o undersampling.

In [7]:
pd_plot_fraud_status = df_base['fraud_bool']\
    .value_counts(normalize=True)\
    .mul(100).rename('percent').reset_index()

pd_plot_fraud_status_conteo = df_base['fraud_bool'].value_counts().reset_index()

pd_plot_fraud_status_pc = pd.merge(pd_plot_fraud_status, 
                                   pd_plot_fraud_status_conteo, left_index=True, right_index=True, how='inner')

fig = px.histogram(pd_plot_fraud_status_pc, x="fraud_bool_x", y=['percent'])

fig.show()

En esta parte del código estamos separando nuestro dataset en train y test para llevar a cabo el análisis de los mismos, de las variables, los null, etc.

In [8]:
X_fraud, X_fraud_test, y_fraud, y_fraud_test = train_test_split(df_base.drop('fraud_bool',axis=1), 
                                                                     df_base['fraud_bool'], 
                                                                     stratify=df_base['fraud_bool'], 
                                                                     test_size=0.2)
df_base_train = pd.concat([X_fraud, y_fraud],axis=1)
df_base_test = pd.concat([X_fraud_test, y_fraud_test],axis=1)
In [9]:
print('== Train\n', df_base_train['fraud_bool'].value_counts(normalize=True))
print('== Test\n', df_base_test['fraud_bool'].value_counts(normalize=True))
== Train
 fraud_bool
0    0.988971
1    0.011029
Name: proportion, dtype: float64
== Test
 fraud_bool
0    0.98897
1    0.01103
Name: proportion, dtype: float64

En este apartado hemos dividido el dataset en train y test. Vamos a usar train, que es la parte del dataset que utilizaremos para entrenar el modelo. Por otro lado la parte test, es la parte del dataframe que usaremos para validar el modelo y observar como se comporta. En este paso también se puede observar que los tanto el test como el train tienen la misma proporción de Fraud_bool=0 que Fraud_bool=1.

Visualización descriptiva de los datos¶

Veo el número de valores nulos por filas y por columnas

In [10]:
base_series_null_columns = df_base_train.isnull().sum().sort_values(ascending=False)
base_series_null_rows = df_base_train.isnull().sum(axis=1).sort_values(ascending=False)
print(base_series_null_columns.shape, base_series_null_rows.shape)

base_null_columnas = pd.DataFrame(base_series_null_columns, columns=['nulos_columnas'])     
base_null_filas = pd.DataFrame(base_series_null_rows, columns=['nulos_filas'])  
base_null_filas['target'] = df_base['fraud_bool'].copy()
base_null_columnas['porcentaje_columnas'] = base_null_columnas['nulos_columnas']/df_base_train.shape[0]
base_null_filas['porcentaje_filas']= base_null_filas['nulos_filas']/df_base_train.shape[1]
(32,) (800000,)
In [11]:
base_null_columnas
Out[11]:
nulos_columnas porcentaje_columnas
income 0 0.0
name_email_similarity 0 0.0
month 0 0.0
device_fraud_count 0 0.0
device_distinct_emails_8w 0 0.0
keep_alive_session 0 0.0
device_os 0 0.0
session_length_in_minutes 0 0.0
source 0 0.0
foreign_request 0 0.0
proposed_credit_limit 0 0.0
has_other_cards 0 0.0
bank_months_count 0 0.0
phone_mobile_valid 0 0.0
phone_home_valid 0 0.0
housing_status 0 0.0
email_is_free 0 0.0
credit_risk_score 0 0.0
employment_status 0 0.0
date_of_birth_distinct_emails_4w 0 0.0
bank_branch_count_8w 0 0.0
velocity_4w 0 0.0
velocity_24h 0 0.0
velocity_6h 0 0.0
zip_count_4w 0 0.0
payment_type 0 0.0
intended_balcon_amount 0 0.0
days_since_request 0 0.0
customer_age 0 0.0
current_address_months_count 0 0.0
prev_address_months_count 0 0.0
fraud_bool 0 0.0
In [12]:
base_null_filas.head()
Out[12]:
nulos_filas target porcentaje_filas
944074 0 0 0.0
140149 0 0 0.0
974536 0 0 0.0
40493 0 0 0.0
240683 0 0 0.0
In [13]:
variables = ['prev_address_months_count', 'intended_balcon_amount', 'bank_months_count', 'session_length_in_minutes', 'device_distinct_emails_8w']
missing_count = []
missing_percentage = []

for variable in variables:
    if variable == 'intended_balcon_amount':
        missing_count.append(df_base_train[df_base_train[variable] < 0].shape[0])
    else:
        missing_count.append(df_base_train[df_base_train[variable] == -1].shape[0])

    missing_percentage.append((missing_count[-1] / len(df_base_train)) * 100)

missing_data_df = pd.DataFrame({
    'Variable': variables,
    'Missing': missing_count,
    'Porcentaje Missing': missing_percentage
})

missing_data_df
Out[13]:
Variable Missing Porcentaje Missing
0 prev_address_months_count 570465 71.308125
1 intended_balcon_amount 593785 74.223125
2 bank_months_count 202866 25.358250
3 session_length_in_minutes 1622 0.202750
4 device_distinct_emails_8w 296 0.037000

Como se puede observar vemos que ninguna fila o columna contiene valores NULL o 0. Pero en la leyenda del dataset se nos informa de que los nulos ya han sido previamente tratados por lo cual no hace falta realizar ese proceso. Como podemos observar más del 70% de las variables _prev_address_monthscount e _intended_balconamount.

Distribución del resto de variables¶

In [ ]:
for i in list(df_base_train.columns):
    if (df_base_train[i].dtype==float) & (i!='fraud_bool'):
        plot_feature(df_base_train, col_name=i, isContinuous=True, target='fraud_bool')
    elif  i!='fraud_bool':
        plot_feature(df_base_train, col_name=i, isContinuous=False, target='fraud_bool')

Podemos observar que la gran mayoría de nuestros clientes tienen de 20 a 50 años. Después, en el gráfico en el que se relaciona la varible customer_age y fraud_bool, podemos observar que el customer_age de la mayoría de los fraud_bool=1 es mayor de 60. Teniendo un pequeño pico en los 80 años. Esto nos puede ayudar a crear un perfil con las características que tiene un cliente fraudulento. Estos gráficos no nos aportan más información por el problema antes mencionado, ya que hay muy pocos valores fraud_bool=1 en comparación con los fraud_bool=0.

Tratamiento de las variables continuas¶

A continuación, se tratan los valores missing, las correlaciones de las vairbales continuas y los outlier

In [ ]:
list_var_continuous

Tratamiento de outliers¶

Los valores outlier se pueden sustituir por la media, mediana, valores extremos (media+3std o media-3std). Tras el siguiente análisis, he decidido como primera iteración dejarlos sin sustituir. Una vez llegue al modelo puedo realizar iteraciones utilizando diferentes métodos para comprobar si mejora el modelo

In [ ]:
get_deviation_of_mean_perc(df_base_train, list_var_continuous, target='fraud_bool', multiplier=3)

Los valores outlier son muy bajos por los que actualmente no vamos a tratarlos ya que no afectan al análisis del dataset.

Correlaciones¶

In [ ]:
get_corr_matrix(dataset = df_base_train[list_var_continuous], 
                metodo='pearson', size_figure=[10,8])

Como podemos observar las variables que más correlación tienen entre sí son variables que dependen unas de otras ya que dependen del factor tiempo pero indican lo mismo, en este caso mide la velocidad en la que se han hecho las solicitudes. Del resto de variables no podemos destacar nada. Sin tener en cuenta las variables mencionadas la siguiente que mayor correlación tienen es el proposed_credit_limit y el income. La correlacion que tienen es de 0.1. No es muy alta.

In [ ]:
corr = df_base_train[list_var_continuous].corr('pearson')
new_corr = corr.abs()
new_corr.loc[:,:] = np.tril(new_corr, k=-1) # below main lower triangle of an array
new_corr = new_corr.stack().to_frame('correlation').reset_index().sort_values(by='correlation', ascending=False)
new_corr

Tratamiento de valores nulos¶

Estas son las variables que tienen valores missing que estan anotados en la leyenda del dataset:

  1. prev_adress_months_count (-1)
  2. intended_balcon_amount (negatives are missing values).
  3. bank_months_count (-1)
  4. session_length_in_minutes (-1)
  5. device_distinct_emails (-1)
In [ ]:
list_var_continuous
In [ ]:
get_percent_null_values_target(df_base_train, list_var_continuous, target='fraud_bool')

Dentro de nuestro dataset no se han encontrado valores nulos, pero como antes hemos mencionado si existen. No estan como nulos si no que han querido reemplazarlos.

Tratamiento de las variables categoricas¶

In [ ]:
list_var_cat
In [ ]:
confusion_matrix = pd.crosstab(df_base_train["fraud_bool"], df_base_train["customer_age"])
print(confusion_matrix)
cramers_v(confusion_matrix.values)

Si hacemos la correlación de una variable consigo misma sale una correlación del 1. Ya que son la misma variable por ende estan muy correlacionadas entre sí.

In [ ]:
confusion_matrix = pd.crosstab(df_base_train["fraud_bool"], df_base_train["fraud_bool"])
cramers_v(confusion_matrix.values)

Guardado de la tabla¶

In [ ]:
df_base_train.to_csv("./df_base_train.csv")
df_base_test.to_csv("./df_base_test.csv")
In [ ]:
print(df_base_train.shape, df_base_test.shape)